package raytracer.geometry;

import java.util.List;
import java.util.Vector;

import raytracer.file.RT_Object;
import raytracer.geometry.*;

public class SceneNode 
{
	public List<SceneNode> childs;
	
	public float[] min = new float[3];
	public float[] max = new float[3];
	public RT_Object rtObject;
	public String caption;
	
	public Transformation transformation;
	
	public SceneNode()
	{
		childs = new Vector<SceneNode>();
		rtObject = null;
		transformation = null;
	}
	
	public void prepareScene(float [] rayE, float [] centerOfLight)
	{
		prepareScene(rayE, centerOfLight, new Identity());
	}
	
	public void prepareScene(float [] rayE, float [] centerOfLight, Transformation matrix)
	{
		// prepare transformations of all childs
		if (this.transformation != null)
		{
			matrix = matrix.combine(transformation);
		}
		
		if (this.rtObject != null)
		{
			
			rtObject.applyTransformation(matrix);
			rtObject.prepareData(rayE, centerOfLight);
		}
		
		// combine transformation to childs
		for (SceneNode node: childs)
		{
			node.prepareScene(rayE, centerOfLight, matrix);
		}
		
		// prepare bounding box for this scene node (and subsequently all it's childs)
		prepareBoundingBox();
	}
	
	private void prepareBoundingBox()
	{		
		if (rtObject != null)
		{
			min[0] = rtObject.min[0];
			min[1] = rtObject.min[1];
			min[2] = rtObject.min[2];
			
			max[0] = rtObject.max[0];
			max[1] = rtObject.max[1];
			max[2] = rtObject.max[2];
		}
		
		if (childs.size() > 0)
		{
		
			SceneNode child = childs.get(0);
			child.prepareBoundingBox();
			
			if (rtObject == null)
			{
				min[0] = child.min[0];
				min[1] = child.min[1];
				min[2] = child.min[2];
				
				max[0] = child.max[0];
				max[1] = child.max[1];
				max[2] = child.max[2];
			}
			
			// prepare bounding box around childs
			for (int i = 0; i < childs.size(); i++)
			{
				child = childs.get(i);
				//child.prepareBoundingBox();
				
				for (int j = 0; j < 3; j++)
				{
					if (child.min[j] < min[j])
					{
						min[j] = child.min[j];
					}
					
					if (child.max[j] > max[j])
					{
						max[j] = child.max[j];
					}
				}
			}
		}
	}
	
	public boolean testBoundingBox(float rayEx, float rayEy, float rayEz, float rayVx, float rayVy, float rayVz)
	{
		float pen, t;
		float [] ip = new float[3];
		
		// "rayVn" == 0 => parallel to plane / case n = (0, 0, 1) & n = (0, 0, -1)
		// if intersection with yx plane
		if (Math.abs(rayVz) > 1E-5)
		{
			// test front plane
			    pen = (max[2] - rayEz);
				
			    // calculate intersection point with plane along the ray
			    t = pen / rayVz;
		
			    // the intersection point with the plane
	    		// the intersection point s(t) = RayE + t * RayV
	    		ip[0] = rayEx + t * rayVx;
	    		ip[1] = rayEy + t * rayVy;
	    		ip[2] = rayEz + t * rayVz;
	    		
	    		if (ip[0] >= min[0] && ip[0] <= max[0] && ip[1] >= min[1] && ip[1] <= max[1])
	    		{
	    			return true;
	    		}
	    		
    		// test back plane	    		
	    		pen = - (min[2] - rayEz);	// n = (0, 0, -1) => -
	    		
	    		// calculate intersection point with plane along the ray
			    t = pen / rayVz * -1;
			    
			    // the intersection point with the plane
	    		// the intersection point s(t) = RayE + t * RayV
	    		ip[0] = rayEx + t * rayVx;
	    		ip[1] = rayEy + t * rayVy;
	    		
	    		if (ip[0] >= min[0] && ip[0] <= max[0] && ip[1] >= min[1] && ip[1] <= max[1])
	    		{
	    			return true;
	    		}
		}
			
		// "rayVn" == 0 => parallel to plane / case n = (0, 1, 0) & n = (0, -1, 0)
		// if intersection with xz plane
		if (Math.abs(rayVy) > 1E-5)
		{
			
			// test upper plane		
				pen = (max[1] - rayEy);
				
				// calculate intersection point with plane along the ray
			    t = pen / rayVy;
		
			    // the intersection point with the plane
	    		// the intersection point s(t) = RayE + t * RayV
	    		ip[0] = rayEx + t * rayVx;
	    		ip[2] = rayEz + t * rayVz;

	    		    		
	    		if (ip[0] >= min[0] && ip[0] <= max[0] && ip[2] <= max[2] && ip[2] >= min[2])
	    		{
	    			return true;
	    		}
	    			    		
    		// test lower plane				
				pen = - (min[1] - rayEy);
				
				// calculate intersection point with plane along the ray
				// t = (p - e) * n / rayV * n => n = (0, -1 !, 0)
			    t = pen / rayVy * -1;
		
			    // the intersection point with the plane
	    		// the intersection point s(t) = RayE + t * RayV
	    		ip[0] = rayEx + t * rayVx;
	    		ip[2] = rayEz + t * rayVz;  		
	    		
	    		if (ip[0] >= min[0] && ip[0] <= max[0] && ip[2] <= max[2] && ip[2] >= min[2])
	    		{
	    			return true;
	    		}		
		}
		
		// "rayVn" == 0 => parallel to plane / case n = (-1, 0, 0) & n = (1, 0, 0)
		// if intersection with zy plane
		if (Math.abs(rayVx) > 1E-5)
		{			
			// test left plane	
				pen = - (min[0] - rayEx);
				
				t = pen / rayVx * -1;
				
				// the intersection point with the plane
	    		// the intersection point s(t) = RayE + t * RayV
	    		ip[1] = rayEy + t * rayVy;  
	    		ip[2] = rayEz + t * rayVz;
	    		
	    		if (ip[1] >= min[1] && ip[1] <= max[1] && ip[2] <= max[2] && ip[2] >= min[2])
	    		{
	    			return true;
	    		}
	    		
	    		
			// test right plane				
				pen = (max[0] - rayEx);
				
				t = pen / rayVx;
				
				// the intersection point with the plane
	    		// the intersection point s(t) = RayE + t * RayV
	    		ip[1] = rayEy + t * rayVy;  
	    		ip[2] = rayEz + t * rayVz;
	    		
	    		if (ip[1] >= min[1] && ip[1] <= max[1] && ip[2] <= max[2] && ip[2] >= min[2])
	    		{
	    			return true;
	    		}
			
		}
		
		return false;		
	}
	
	/**
	 * Traces the ray and returns all objects which bounding box was hit.
	 * 
	 * @return
	 */
	public List<RT_Object> traceRayAndGetObjects(float rayEx, float rayEy, float rayEz, float rayVx, float rayVy, float rayVz)
	{
		if (!testBoundingBox(rayEx, rayEy, rayEz, rayVx, rayVy, rayVz))
		{
			// if no hit in this nodes bounding box, no child bounding box will hit
			// return with no objects
			return null;
		}
		
		List<RT_Object> objects = new Vector<RT_Object>();
		List<RT_Object> childObjects;
		
		if (rtObject != null)
		{
			objects.add(rtObject);
		}
		
		SceneNode child;
		for (int i = 0; i < childs.size(); i++)
		{
			child = childs.get(i);
			
			if (child.testBoundingBox(rayEx, rayEy, rayEz, rayVx, rayVy, rayVz))
			{
				childObjects = child.traceRayAndGetObjects(rayEx, rayEy, rayEz, rayVx, rayVy, rayVz);
				objects.addAll(childObjects);
			}
		}
		
		return objects;
	}
}
